Archive | November 2010

Using named DATE_TIME formats

How many times do you find yourself doing something like this:

DateTime.now.strftime('%m-%d-%Y')

Ugh! Ugly and not very DRY!

Instead, create a file called datetime_format.rb in your config/initializers folder. Put the following snippet into that file:

ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(
  :year_month_day => '%Y-%m-%d',
  :year_month_day_and_time => '%Y-%m-%d %H:%M'
)

Now, instead of that ugly line above, you can use the following:

DateTime.now.to_s(:year_month_day)
DateTime.now.to_s(:year_month_day_and_time)

Ah, much better. We now have readable maintainable date formats all defined in a single place.

Append the contents of one file to another

I often have to append a file to another file, usually when I’m adding a new key to a server’s authorized_keys file. So, here’s an example…

small_file.txt contains the following text:

my secret key

large_file.txt contains the following text:

some other secret key

To append the contents of small_file.txt to large_file.txt run the following command:

cat small_file.txt >> large_file.txt

That will result in the contents of large_file.txt looking as follows:

some other secret key
my secret key

Conditional validation using ActiveRecord

ActiveRecord validations are easy, right? But what if you need a validation to happen based on a condition?

One way of doing this is to roll your own validation method like so…

class Account < ActiveRecord::Base
  validate :cancelled?

  def cancelled?
    if condition_is_true
      validates_presence_of :cancellation_reason
    end
  end
end

Effective but a bit clunky. My preferred approach is using ActiveRecord’s with_options as follows…

class Account < ActiveRecord::Base
  with_options :if => :cancelled? do |this|
    this.validates_presence_of :cancellation_reason
  end

  private
  def cancelled?
    # some clever stuff here...
  end
end

Speed up your features with a backdoor login route

In any application that tracks users, I’m guessing that the vast majority of features will require that a user is logged in. This however can add a huge amount of overhead to your features at run time.

To speed things up, you can create a backdoor route to allow your features to bypass the login page and form submission, and simply hit a url to create the session.

First, define the route scoped by a condition – you don’t want this in production mode!

if %w( development test cucumber ).include?(Rails.env.to_s)
  map.backdoor '/backdoor', :controller => :user_sessions, :action => :backdoor
end

Then define the controller action. Bear in mind we are using authlogic. Adjust this to suite your own requirements.

# for cucumber testing only
def backdoor
  @user_session = UserSession.new(User.find_by_email(params[:email]))
  @user_session.save
  head 'ok'
end

Finally, your cucumber step goes from something like this…

Given /^I authenticate successfully$/ do
  Given 'I am on the login page'
  And 'I fill in "Email" with "eddie@hotmail.com"'
  And 'I fill in "Password" with "secret"'
  When 'I press "Login"'
  Then 'I should see "Welcome, Service Account (AD)"'
end

…to something like this…

Given /^I am already logged in as a user$/ do
  visit "/backdoor?email=eddie@hotmail.com"
end

We shaved nearly 2 minutes off our 12-minute build!

Thanks to this post for the original idea.

Find your slowest-running cucumber features

Our feature set has 275 scenarios with 1175 steps, and it’s growing every day. In total it takes about 11 minutes to run the full set of features. We wanted identify the 20 slowest scenarios so that we could do some refactoring both in our code and tests to make things run a bit faster.

Add the following code to your features/support/env.rb file. All we do is catch each scenario using the Around function and grab the execution time for each scenario. Then display he results at the end of your build process.

scenario_times = {}

Around() do |scenario, block|
  start = Time.now
  block.call
  scenario_times["#{scenario.feature.file}::#{scenario.name}"] = Time.now - start
end

at_exit do
  max_scenarios = scenario_times.size > 20 ? 20 : scenario_times.size
  puts "------------- Top #{max_scenarios} slowest scenarios -------------"
  sorted_times = scenario_times.sort { |a, b| b[1] <=> a[1] }
  sorted_times[0..max_scenarios - 1].each do |key, value|
    puts "#{value.round(2)}  #{key}"
  end
end

This will give you output that looks like this:

------------- Top 20 slowest scenarios -------------
9.49   features/client_mapping.feature::Map clients with invalid upload file
8.42   features/client_mapping.feature::Map clients with valid upload file
7.35   features/indicators.feature::Show indicators after client upload
4.50   features/manage_clients.feature::Delete client
..
..
..

Hope this helps someone.

Stub your external endpoints using WebMock

On one of my projects we integrate with 2 external data suppliers through their APIs. In today’s development world this is fairly common and quite easy to do in terms of the integration.

However, the tricky part is figuring out how to test this properly. Using RSpec it’s easy to test models by stubbing out the functions which call the external services. However, when we run our cucumber features we can’t simply stub a method call on a model.

Hello WebMock

WebMock allows you to stub out your external endpoints at the HTTP level. In other words, you stub the external service and response and then allow your application to perform the remote request normally. Within the scope of your application this is a real cucumber feature i.e. an integration test.

Here’s how we use it…

Add the following code to your features/support/env.rb file:

require 'webmock/rspec'
World(WebMock::API, WebMock::Matchers)
WebMock.allow_net_connect!

The final line tells WebMock to allow all requests to proceed to the network as expected. This is important because we are running cucumber features.

Next, in the relevant cucumber step we will specify the behaviour for WebMock.

Given /^the remote service will return a valid response$/ do
  stub_request(:any, 'http://localhost:4567').to_return do |request|
    body = CGI::unescape(request.body)
    code = body.match(/<CODE_NBR>(.*)<\/CODE_NBR>/)
    response = IO.read("#{RAILS_ROOT}/test/responses/#{code[1]}.xml")
    {:body => response, :status => 200}
  end
end

By passing a block to the stub_request method we can make WebMock behave in a more dynamic way, without having to stub out each individual request. In this case, we search the request body for a known tag which contains a unique code. We use that code to return an expected XML response we have saved. That’s it! Our cucumber features call our external services and process the responses as expected.

What did we do before?

We previously had a Sinatra app which would return the same pre-rolled responses used above. The behaviour was essentially the same, but it was complicated and it made our cucumber steps harder to follow and debug. It also meant for each test run we had to kick off and tear down WEBrick processes to run Sinatra properly – ugh! It was very clever but the kind of thing you have to re-learn every time you look at it.

And now…

Our features are now much cleaner, a little faster, but most importantly, readable and maintainable.

Install gems without documentation

I’ve yet to meet a Rails developer who either looks at or even needs the local gem documentation. All it does is slow down the installation of gems.

You could use the relevant switches as follows…

gem install nokogiri --no-rdoc --no-ri

…or you could simply add the following line to the .gemrc file in your home directory:

gem: --no-rdoc --no-ri

Then you can freely install gems without using swtiches and without any time-consuming documetation downloads:

gem install nokogiri

Simples!

Power-up your bash file

I have made my life considerably easier by adding some of my most-used bash commands to by bash_aliases file. Just remember to load this file in your bashrc file and you’re all set…

alias dev="cd ~/Development"

alias ssh_g="ssh user@10.193.152.32"

# console coloring
export PS1='\[\033[1;35m\]\h\[\033[1;33m\] \W\[\033[00m\] => '

# root file browser
alias ste="sudo gedit"
alias sn="sudo nautilus"

# edit bashrc
alias bn="nano ~/.bashrc"
alias bna="nano ~/.bash_aliases"

# edit hosts
alias hosts="sudo nano /etc/hosts"

# free memory
alias free="free -m"

# system helpers
alias update="sudo aptitude update"
alias install="sudo aptitude install"
alias upgrade="sudo aptitude update && sudo aptitude safe-upgrade"
alias remove="sudo aptitude remove"
alias clean="sudo aptitude clean"
alias search="sudo aptitude search"

# reload bash aliases
alias reload="source ~/.bashrc"

# git helpers
alias gu="git pull"
alias gp="git push"
alias ga="git add ."
alias gc="git commit -m \$1"
alias gs="git status"
alias gi="nano .gitignore"
alias gg="git gui &"
alias gk="gitk &"

alias gu_all="sso && gu && rd && gu && rt & gu & ws & gu"

# git config (globally)
alias ggmyname="git config --global user.name \$1"
alias ggmyemail="git config --global user.email \$1"

# git config (locally)
alias gmyname="git config user.name \$1"
alias gmyemail="git config user.email \$1"

# test cucumber features
alias ct="cucumber features -n"

# start ruby
alias rs="ruby script/server"
alias rc="ruby script/console"

# disk space and cls/clear
alias left="df -h"
alias cls="clear"

# flush dns cache
alias flushdns="sudo /etc/init.d/nscd restart"
alias installdnscache="sudo aptitude install nscd"

# logs
alias ld="tail -f log/development.log"
alias lp="tail -f log/production.log"
alias lt="tail -f log/test.log"
alias lc="tail -f log/cucumber.log"
alias lr="rm log/*.log"

# apache
alias ame="sudo a2enmod \$1"
alias amd="sudo a2dismod \$1"
alias ase="sudo a2ensite \$1"
alias asd="sudo a2dissite \$1"
alias arc="sudo /etc/init.d/apache2 reload"
alias arg="sudo apache2ctl graceful"
alias ars="sudo /etc/init.d/apache2 restart"

# capistrano
alias capsetup="cap deploy:setup"
alias capcheck="cap deploy:check"
alias capcold="cap deploy:cold"
alias capstop="cap deploy:stop"
alias capstart="cap deploy:start"

# rake
alias migrate="rake db:migrate"
alias migrate_t="rake db:migrate RAILS_ENV=test"
alias migrate_all="migrate && migrate_t"
alias redo="rollback && migrate"
alias rollback="rake db:rollback"
alias rollback_t="rake db:rollback RAILS_ENV=test"
alias redo_t="rollback_t && migrate_t"

# processes
alias pav="ps aux | grep \$1"

Simples!

Follow

Get every new post delivered to your Inbox.