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.
Does the Chronic Time Parsing Library Break with DST Changes?
I’ve been troubleshooting a problem on an existing application for the last week or so that deals with the parsing of dates using the Chronic time parsing library. Today the problem magically solved itself, without me doing anything. These types of self solving problems are usually more frustrating than problems you can’t solve at all, so I took a little extra time to experiment with the particular date format I was using and found what might be a problem with the Chronic library when it gets to the “fall back” DST change in the fall.
Do You Observe DST?
It looks like if your local environment is set to a timezone which observes DST, like Eastern Standard Time for instance, you get a chunk of dates around the switch in the fall where Chronic returns nil instead of the correct date. If you’re on a machine where the timezone is set to an area which does not observe DST, like Arizona, you won’t be able to replicate this problem.
Replicating the problem
Install the latest version of Chronic.
$> sudo gem install chronic
Fire up irb and give the following a shot.
1 2 3 4 | require 'chronic' (Date.parse("2009-01-01")..Date.parse("2009-12-31")).each do |d| puts Chronic.parse("next tuesday 6am", :now => d) end |
With ruby 1.8.7 (2008-08-11 patchlevel 72) [i486-linux] on Ubuntu and my timezone set to EST/New York. I see a bunch of Tuesdays and then a blank section around the end of October / beginning of November.
...snip... Tue Oct 20 06:00:00 -0400 2009 Tue Oct 20 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 nil nil nil nil nil nil Tue Nov 03 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 ...snip...
If you change your timezone to something like MST/Arizona you’ll see this
...snip... Tue Oct 20 06:00:00 -0400 2009 Tue Oct 20 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Oct 27 06:00:00 -0400 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 03 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 10 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 Tue Nov 17 06:00:00 -0500 2009 ...snip...
What’s the Fix?
None as far as I know. I’m sure one could dig into the internals of the library and figure out how to deal with this problem, but I’m not up for that right now.
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)
RSpec Shared Example before(:each) Gotcha
Shared example groups are a great feature of Rspec that help you simplify your tests and keep your code DRY. You setup shared example groups almost exactly like you would a regular set of specs, but these similarities can be slightly misleading.
Below we have an example model, spec and shared example group. Our Dog model has its own set of functionality, but as a mammal it should still have some aspects of being a mammal. We’ve got some specs in a shared example group that we use for testing all of our mammal models to make sure things don’t get too out of whack in the universe.
Our Example Model
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Dog attr_accessor :name, :mammal def initialize self.mammal = true end def greet "Hi, I'm #{self.name}, woof woof!" end end |
Our Example Spec
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | describe Dog do before(:each) do @animal = Dog.new @animal.name = "Bruno" end it_should_behave_like "a mammal" describe "Greet" do it "should respond with its name and a greeting" do @animal.greet.should == "Hi, I'm Bruno, woof woof!" end end end |
Our Shared Spec
1 2 3 4 5 | describe "a mammal", :shared => true do it "should really be a mammal" do @animal.mammal.should be_true end end |
A Typical before(:each)
Typically, when you’ve got a describe block, you might use before(:each) to setup some scenario that is used for each spec in that describe block, pretty normal RSpec stuff. We’re using it above in our example spec to create a new Dog object and set that dog’s name.
Using before(:each) in a shared spec
What if you wanted to use a before(:each) in your shared spec? Expanding on our example above we can do something like this.
1 2 3 4 5 6 7 8 | describe "a mammal", :shared => true do before(:each) do @animal.stub!(:has_body_hair?).and_return(true) end it "should really be a mammal" do @animal.mammal.should be_true end end |
Based on typical RSpec behavior, one would think that the stubbing of the has_by_hair? method on the instance of an animal, would only apply to the specs inside of the describe block of the shared example group. However, by specifying in the Dog spec that a dog “should behave like” a mammal, and thus using the shared spec, that stub will apply to all subsequent “it should” blocks in your model spec.
What if, for example, we had the following in our Dog spec.
describe "Mutate into Lizard Dog" do # dog.mutate will remove body hair and make the dog cold blooded it "should mutate into a new species" do @animal.mutate @animal.has_body_hair?.should be_false end end
If we include this in our Dog spec, below the inclusion of the shared example spec, our test will fail. We’ve already stubbed out the has_body_hair? method as part of our shared example group, when we call it down here in this completely separate describe block, RSpec is just using the stub we setup previously.
It might be a design problem if…
Now while I’m considering this a gotcha, it may be that this is expected behavior, I couldn’t find anything specifically when researching this “bug” originally. It is also possible that stubbing behavior in shared example groups is frowned upon, and I’m just “doing it wrong”.
Ultimately, I tried using patterns that made sense to me and seemed to be in line with how RSpec works in general. A stubbed method inside the before(:each) of a describe block is usually only applicable to the specs and nested describes contained within. When I realized that this is not the case with shared example groups, it seemed like a gotcha.
Flog as a Surrogate Pair
I’m a fan of Flog. I like using it every now and then to make sure that I’m not getting too complex, trying to be clever, or setting myself up for testing failure. Typically these are things I look to my MILF slaying pair for guidance, but sometimes Flog can be just as good.
As a relative newcomer to Ruby, I’m still in the process of learning to identify code smells, poor design and needless complexity. I’m sold on the “readable code is better” idea so I try to keep that in mind as I’m implementing some piece of functionality. Over at Integrum, where I work, we pair on everything each day, save for the occasional sick day. Today I was working alone and was reviewing some code that my pair and I had wrote about a week earlier. It was complex, and I feel that we made a compromise with the idea that the “ugly” working code was good enough for what we needed at the time, but it was ugly enough that I remembered to revisit it.
Flog Builds Confidence
When I really started looking at the code my intuition told me that it needed to be re-factored. I knew that it could be improved, but I suppose I’ve been in the pair mindset so heavily since I’ve been here that I did not immediately jump in and fix things. However, when I ran Flog and saw the comparatively high scores for two complex methods I had been investigating, I had that supportive “yea I agree” feeling that I usually get from pairing. Flog helped push me over the threshold, from “maybe I should change this” to “this should be changed”.
This isn’t to say that I wouldn’t have changed it without Flog, but it gave me that extra confidence in knowing that I was on the right track. The direct benefit was that the code is now more readable and easier to test. Indirectly I’ve added another point to my mental Ruby experience score and will be more inclined to follow my intuition in the future.
Using RSpec to Test system calls with %x and backticks (grave accents)
I’ve been working on a project that requires a call to the shell from inside a controller. I knew that I would need the output of the shell command, in this case a rake task, so that I could display the result to the user. However, when I went to implement the spec, I wasn’t sure how to setup the expectation.
Our Controller
1 2 3 4 5 6 7 8 | InfoController < ApplicationController def index flash[:notice] = `cat /home/clayton/info` # sets the notice to "clayton@lengelzigich.com" redirect to people_url end end |
Our Spec
1 2 3 4 5 6 7 8 | describe InfoController do describe "index" do it "should set the contents of the flash notice to clayton's contact info" do controller.should_receive(:'`').with("cat /").and_return("clayton@lengelzigich.com") get :index end end end |
The important part of the spec is line 43. We tell the controller to expect a call to '`' with our shell command and return the contents of the file. The '`':, backtick1, is a method in the Kernel class2. It is also possible to use @%x@ to run commands in the shell from ruby, the two are the same.
`cat /etc/motd` # is the same as %x[cat /etc/motd]
If you are using %x@, and need a way to write a spec, consider changing @%x to @“@ and using the above approach.
1 Some people call backticks “grave accents”, some people are dumb.
2 The @`@ method that comes from Kernel isn’t actually called on the Kernel class, it’s mixed into your Object at run time. Or so I’ve read.
3 Some credit for this discovery goes to my straight up ballin’ pair.
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? |
Adaptive, Iterative and Agile Development is Fantastic
The ability to work in short bursts of agility leads to amazing results. The ability to quickly adapt to changing needs brings satisfaction. The ability to combine all of these with Ruby on Rails makes for a happy developer. Adaptive, iterative and agile development with ruby on rails is fantastic!
What I Want Right Now
When I “launched” this blog a few weeks ago I had the ability to create categories and write posts. That’s it. No comments, no tags (still don’t have those) no permalinks, no photos no nothing. Just a really simple system of posting in various categories. This was fine for a while, but I wanted more. So, as time permitted I slowly added new features. Here’s a rough timeline with some made-up version numbers
1.0 - posts and categories 1.1 - user accounts 1.2 - polished admin interface 1.3 - permalinks 1.5 - comments 1.6 - admin comments 1.7 - new comment notifications 1.8 - photos
Together these would have taken a while, but as separate pieces they were simple digestible easy to complete tasks, perfect for my workflow.
Ship It, LDO
I would have loved to have all of these things and more when I launched originally but it wasn’t in my time budget so I had to break it up this way. I just needed to get something up and running, so I went with the most basic of basics. Turns out, I found some things that I could do with out right away (tags, search, ratings) and found some things that I wanted (permalinks, comments, photos). Everything bubbled up according to it’s importance and so far everything feels just right. When it doubt, just ship it. Like duh, obviously.
Rails Tie-In
If you haven’t realized quite yet, Ruby on Rails makes this agile/iterative approach super easy. Between migrations, built-in helpers and BDD I was able to add all of this new functionality piece by piece knowing that I wasn’t going to break anything I already had. Plus it didn’t take very long (learning curve not withstanding).
In a world dictated by client requests and boss-based to-do’s I don’t often get the chance to work like this, but for now I’m cherishing every moment.
Learning Best Practices, Idioms and New Languages
A few days ago I was listening to an episode of the StackOverflow Podcast and Jeff Atwood said something interesting about how he did a lot of reading as a kid but didn’t do much discussing about said reading with other people. The problem being that he had a lot of words that he wasn’t sure how to pronounce outside of how he had originally determined they should be pronounced. I was thinking about how this translates to code constructs and programming styles and how it’s easy to get stuck in your newb ways.
How Do You Say That?
I’m sure we’ve all experienced what Jeff was talking about, you’re reading something and you come across the name of a person or a place and you’re not really sure how to pronounce it but for the sake of moving through the material your brain comes up with something and you continue to use that each consecutive time it comes up in the material.
It’s only the next day when you’re talking about the text with a friend that you hear their alternate pronunciation of the word. Sometimes neither or you are sure what the correct pronunciation is, but you now spend the rest of the conversation saying each pronunciation, so as not to be rude, when referring to the word in question.
Learning PHP
A lot of programmers start out learning PHP. It’s easy to learn, make something, lots of resources etc. etc. However, I think that you tend to do a lot of the “brain deciding how to do this previously unknown thing” decision making while you’re learning. There was a post on The Daily WTF today about somebody who basically re-implemented the functionality of $_GET using their own hacked together POS method. It’s good for a laugh sure, but it’s not that unreasonable when you think about it.
Imagine you’re learning PHP and you’re like most geeks, introverted, you want to accomplish this goal of learning a new language, but you’re not planning on spending any time discussing it with other people. This is especially likely if you’re not learning in a work environment with other people. Since you don’t know a lot of the idioms, best practices and commonly used syntax you end up figuring things out and deciding that’s just how it’s done. I only use PHP in this example because of its wide use and forgiveness when not sticking to accepted practices (are there any?).
You find ways to do things and you stick with them, for good or bad, until you find out a different way.
Quit Being an Introvert
Do yourself a favor and get over keeping to yourself, you’ll be a better programmer and you might learn something about social interaction.
- Join mailing lists (*and participate*)
- Join forums (*and participate*)
- Attend local in-person live events
- Find some AIM/YM/MSN/Gtalk buddies who you can bounce ideas off of
- Seek the advice of the more experienced
