Thursday, September 27, 2012

RSpec matcher for model attributes

Have you ever wanted to check that the attributes of a model have been defined correctly?

You may have created a model Store as follows:

class Create'Stores < ActiveRecord::Migration
  def change
    create_table :stores do |t|
      t.string :title
      t.string :description, :null => false
      t.string :url, :limit => 1024
      t.boolean :open
    end
  end
end
Imagine that there was a have_attribute matcher, that allowed you to write specs for each of these attributes as follows:
require 'spec_helper'

describe Store do
  it { should have_attribute(:title) }
  it { should have_attribute(:description).with(:type => :string, :null => false) }
  it { should have_attribute(:url).with(:type => :string, :limit => 1024) }
  it { should have_attribute(:open).with(:type => :boolean) }
end
Good news! Such a matcher now exists in the form a gem. Look at https://rubygems.org/gems/rspec_attribute_matchers.

To make this matcher available in your model specs, do the following:

Gemfile
group :development do
  ...
  gem 'rspec_attribute_matchers', '~> 0.0.0'
  ...
end

spec/spec_helper.rb
require 'rspec_attribute_matchers'

RSpec.configure do |config|
  ...
  config.include RSpecAttributeMatchers, :type => :model
  ...
end

Wednesday, May 16, 2012

Unit testing Blather applications using RSpec


Blather is an easy to use XMPP client library written in Ruby. It seems to be actively developed and supported, as opposed to its competition XMPP4R library. I am using it in an application that has thousands of users interacting with it in real time.

Refer to https://github.com/sprsquish/blather for more details about the library.

This post demonstrates one way of writing concise tests for Blather based applications using RSpec.

There are different ways of writing Blather applications as shown in http://rubydoc.info/github/sprsquish/blather/master/Blather/DSL.

I have chosen the following approach of subclassing the Blather client, as it led to readable clients and simple tests.

#!/usr/bin/env ruby

require "blather/client/client"

class OurClient < Blather::Client
  def initialize
    super

    register_handler :subscription, :request? do |stanza|
      on_subscription(stanza)
    end

    register_handler :message, :chat?, :body do |stanza|
      on_message(stanza)
    end
  end

  def on_subscription(stanza)
    write stanza.approve!   # Approve the subscription request.
    write stanza.request!   # Request subscription in turn.
  end

  def on_message(stanza)
    # Echo the received message back to the sender.
    write Blather::Stanza::Message.new stanza.from, "You sent: #{stanza.body}"
  end
end


if __FILE__ == $0
  trap(:INT) { EM.stop }
  trap(:TERM) { EM.stop }

  client = OurClient.setup "@", "", "", 5222
  EM.run { client.run }
end

The application above demonstrates handling of subscription requests (invitations to add as a friend) and incoming messages. These are the two major interactions that a typical XMPP client engages in.

On receiving an invitation, our client simply accepts the invitation and sends a similar request to the sender. At the end of it, both the invitee and our client are subscribed to each other.

On receiving a message, our client simply echoes the message back, as an acknowledgement of the message.

Writing specs for this client is as simple:

require "spec_helper"
require "our_client"

describe OurClient do
  let(:client) { OurClient.new }
  before { client.stub!(:write) }

  describe "#initialize" do
    it "registers a handler for subscriptions." do
      stanza = Blather::Stanza::Presence::Subscription.new(random_email,
                                                           :subscribe)

      client.should_receive(:on_subscription).with(stanza)
      client.send :call_handler_for, :subscription, stanza
    end

    it "registers a handler for messages." do
      stanza = Blather::Stanza::Message.new(random_email, random_string)

      client.should_receive(:on_message).with(stanza)
      client.send :call_handler_for, :message, stanza
    end
  end

  describe "#on_subscription" do
    it "approves the subscription request." do
      stanza = Blather::Stanza::Presence::Subscription.new(random_email,
                                                           :subscribe)

      client.should_receive(:write).with(stanza.approve!)
      client.on_subscription(stanza)
    end

    it "sends a subscription request." do
      stanza = Blather::Stanza::Presence::Subscription.new(random_email,
                                                           :subscribe)

      client.should_receive(:write).with(stanza.request!)
      client.on_subscription(stanza)
    end
  end

  describe "#on_message" do
    it "echoes the response back." do
      body = random_string
      sender = random_email
      stanza = Blather::Stanza::Message.new(random_email, body)
      stanza.stub!(:from).and_return(sender)

      client.should_receive(:write) do |message|
        message.to.should == sender
        message.body.should == "You sent: #{body}"
      end
      client.on_message(stanza)
    end
  end
end

Sharing Your Mac OS X Desktops Using Google Drive

Here is what my Macbook Desktop looks like:


My iMac desktop in my office also looks exactly like the above. In fact, when I create a file on my Macbook desktop at home, it magically appears on my iMac desktop in the office.

My desktop now lives in the Cloud, thanks to Google Drive. There is no "special folder" that I have to remember putting my files into for them to be synced across all my machines.

How did I do this? Read on!

First, you have to install Google Drive. Go to http://drive.google.com and log into your Google account. Click on the "Get started with 5 GB free" button on the top.
Click on "Download Google Drive" link at the bottom of the left menu and follow instructions to download and install the DMG for Google Drive.

Launch Google Drive from your Application Menu (or Launchpad on OS X Lion) and sign in using your Google account.

Next, launch a terminal and change your Desktop to use Google Drive instead as follows:

$ cd
$ mv Desktop/* Google\ Drive/
$ rm -r Desktop
$ ln -s Google\ Drive/ Desktop

Log out and log back in.

Now your Desktop should start syncing and showing all your files in Google Docs. Any new files or folders that you create in your Desktop from now on will be synced across all machines that have Google Drive set up.

Repeat the aforementioned instructions for all machines which you want to keep in sync and enjoy the ride!

Wednesday, April 25, 2012

Deploying with Capistrano in /mnt on EC2 machines

I use Ubuntu on my EC2 instances. These are based on standard AMIs issued by Ubuntu at http://cloud.ubuntu.com/ami. These instances all have ephemeral storage mounted at /mnt by default, with a size of 145GB for small instances.

Since /mnt is backed by ephemeral storage, this directory is an ideal location for deploying Rails applications, since you will not typically want to include your source controlled application code in any backup of your instances. Besides, /mnt has a large size, compared to a few GBs for the root partition.

My rails apps are deployed using Capistrano at /mnt/deployment/ on the target EC2 instances.

I run my deployment as a non-root user. Since /mnt is mounted as root by EC2, this can be a problem, especially since I do not like to use sudo in deployment scripts unless absolutely necessary. Note that even if you chown /mnt to be owned by something non-root, it will revert to root (and lose all data, being ephemeral) whenever the instance is stopped and started again.

The solution I have found is to add an upstart script, which I named /etc/init/mounted-ephemeral.conf, following the conventions of other such scripts.

$ sudo cat /etc/init/mounted-ephemeral.conf 
start on mounted MOUNTPOINT=/mnt

task

console output

exec chown user.eng /mnt

This script ensures that the ownership of /mnt is always set to the user I want, whenever the machine starts up.

Monday, April 2, 2012

Unit testing emails in Rails using RSpec

In this post I am using a specific example of a controller action that sends an email. However, the outlines of the testing approach should be applicable in a wider variety of cases.

Setup


1. app/controllers/users_controller.rb:

class UsersController < ApplicationController

  def signup
    email = params[:email]
    ...
    ...
    UserMailer.signup(email).deliver
  end
end

2. app/mailers/user_mailer.rb:

class UserMailer < ActionMailer::Base
  default from: "sender@domain.com"

  def signup(email)
    mail(:to => email, :subject => "Welcome!")
  end
end

3. app/views/user_mailer/signup.html.haml (contents of the email):

A quick brown fox jumped over the lazy dog.

There are three parts to writing comprehensive specs for this setup. The first part, corresponding to the controller, should test that the controller indeed sends the correct email. The second part, corresponding to the mailer, should test that the email is set up correctly, i.e, it has the correct sender, recipient, subject and so on. The third part is about testing the contents of the email itself, corresponding to the email view.

Tests

Part I: Is the correct email being sent? (Controller)

In spec/controllers/users_controller_spec.rb:

require "spec_helper"

describe UsersController do
  describe "POST signup" do
    it "sends a verification email." do
      email = random_email

      post :signup, email: email
      expect_email UserMailer.signup(email)
    end
  end
end

The bolded part is key. It is a simple helper method that tests that the email has all the expected headers, since headers define the email.

P.S. "random_email" is another helpful method that I end up using frequently.

My implementation of the helper method, which I define in spec/support/email.rb is as follows:

def expect_email(email)
  delivered = ActionMailer::Base.deliveries.last
  expected =  email.deliver

  delivered.multipart?.should == expected.multipart?
  delivered.headers.except("Message-Id").should == expected.headers.except("Message-Id")
end

The test excludes "Message-Id" from the headers, since that is going to be different for every instance of otherwise exactly identical emails.

Part II: Does the email have the right headers? (Mailer)

In spec/mailers/user_mailer_spec.rb:

require "spec_helper"

describe UserMailer do
  describe ".signup" do
    let(:recipient) { random_email }
    let(:email) { UserMailer.signup(recipient) }

    it { email.from.should == [ "sender@domain.com" ] }
    it { email.to.should == [ recipient ] }
    it { email.subject.should == "Welcome" }
  end
end

The above should be fairly self explanatory.

Part III: Does the email have correct contents? (View)

This is no different from any other view tests, therefore I have omitted this part.

I hope you found this post useful in keeping your code lean and clean.

Saturday, March 31, 2012

Emails in Rails 3 using Amazon SES

Setting up Rails 3.2 for sending emails using Amazon's Simple Email Service (SES) is easy. You do not require any additional gem or monkey patching to make it work.

SES supports both STARTTLS over SMTP as well as TLS/SSL. The following demonstrates how to set up Rails for STARTTLS over SMTP with Amazon SES.


Prerequisites

1. If you are running rails on Mac OS X, you may need to configure OpenSSL for Ruby correctly before you can use STARTTLS. If you are using Ruby 1.9.3 and RVM, here is one way to do this:

rvm pkg install openssl
rvm reinstall 1.9.3 --with-openssl-dir=$rvm_path/usr


If you do not do this, there is a possibility that Ruby will segfault when you try to send an email.

2. Make sure you have verified your sender email address with AWS. You can only send emails with a verified email address as the sender. Go to the "Verified Senders" option on the left menu in AWS console for SES.

3. Make sure you have the AWS SMTP user name and password for authentication. Go to the "SMTP Settings" option on the left menu in AWS console for SES to set this up. You will first be prompted to create an IAM user (default: ses-smtp-user) and then you will be shown the SMTP user and password, which look like usual AWS key and secret. Note that the IAM user, i.e., ses-smtp-user is not the SMTP user that you will be using for authentication.


Configuring Rails

In config/development.rb and config/production.rb, add the following:

  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
          :address => "email-smtp.us-east-1.amazonaws.com",
          :user_name => "...", # Your SMTP user here.
          :password => "...",  # Your SMTP password here.
          :authentication => :login,
          :enable_starttls_auto => true
  }


Sending an email

This is it. Now you can go ahead and create a mailer and start sending emails for fun and profit!
A sample mailer and its usage is shown below:

Create a sample mailer:

rails g mailer user_mailer

In app/mailer/user_mailer.rb:

class UserMailer < ActionMailer::Base
  # Make sure to set this to your verified sender!
  default from: "your@verifiedsender.com"  

  def test(email)
    mail(:to => email, :subject => "Hello World!")
  end
end

In views/user_mailer/test.erb:

A quick brown fox jumped over the lazy dog.

Now, launch the console and shoot off a test email:

rails c

Loading development environment (Rails 3.2.1)
1.9.3p125 :001 > UserMailer.test("your@email.com").deliver

Wednesday, July 20, 2011

Stubbing constants in Rspec

This blog talks about stubbing constants in RSpec. The posted solution only works for global constants.

The following version works with all constants.

def parse(constant)
  source, _, constant_name = constant.to_s.rpartition('::')

  [source.constantize, constant_name]
end

def with_constants(constants, &block)
  saved_constants = {}
  constants.each do |constant, val|
    source_object, const_name = parse(constant)

    saved_constants[constant] = source_object.const_get(const_name)
    Kernel::silence_warnings { source_object.const_set(const_name, val) }
  end

  begin
    block.call
  ensure
    constants.each do |constant, val|
      source_object, const_name = parse(constant)

      Kernel::silence_warnings { source_object.const_set(const_name, saved_constants[constant]) }
    end
  end
end