Smooth RSpec experience with Zeus

I’m working with Rails 4, and though I like the new version of the framework, I miss such handy things as Spork running with it out of the box (yes, you can tweak and patch, but…). So, I turned to other tools out there for speeding up my development/testing environment. And obviously the first thing that came to my attention was Zeus. There is a great post devoted to adding it to your project: http://robots.thoughtbot.com/post/40193452558/improving-rails-boot-time-with-zeus. And it’s pretty simple anyways, so no need to repeat the basic steps here.

Since I use RSpec, don’t use Cucumber, use Pow for starting the Rails server, and I don’t mind to wait for a few extra seconds when I occasionally run rails db (I just do it too infrequently to care), my zeus.json looks a bit simpler that the default:

{
  "command": "ruby -rubygems -r./custom_plan -eZeus.go",

  "plan": {
    "boot": {
      "default_bundle": {
        "development_environment": {
          "prerake": {"rake": []},
          "console": ["c"],
          "generate": ["g"]
        },
        "test_environment": {
          "test_helper": {"test": ["rspec"]}
        }
      }
    }
  }
}

And it works. However, there is one thing that annoys me about the setup. Instead of doing just zeus rspec to run tests, you have to do zeus rspec spec/spec_helper.rb, otherwise you get this:

$ zeus rspec
~/.rvm/gems/ruby-2.0.0-p0/gems/zeus-0.13.3/lib/zeus/rails.rb:214:in `spec_file?': undefined method `match' for nil:NilClass (NoMethodError)

and then a bunch of stack trace lines.

The problem is in how the test method is defined in Zeus code (see https://github.com/burke/zeus/blob/master/rubygem/lib/zeus/rails.rb):

    def test(argv=ARGV)
      if spec_file?(argv) && defined?(RSpec)
        # disable autorun in case the user left it in spec_helper.rb
        RSpec::Core::Runner.disable_autorun!
        exit RSpec::Core::Runner.run(argv)
      else
        Zeus::M.run(argv)
      end
    end

It first attempts to check if we meant RSpec, and only if detects the spec file, proceeds to executing it. Since in my case I know I always mean RSpec, I can avoid that spec_file? check. Fortunately, there is an easy way to do that. Just define your custom method in custom_plan.rb and use it inside your zeus.json instead of the original test method. Like this (note the test -> spec change):

# in custom_plan.rb:
  def spec(argv=ARGV)
    # disable autorun in case the user left it in spec_helper.rb
    RSpec::Core::Runner.disable_autorun!
    exit RSpec::Core::Runner.run(argv)
  end

# in zeus.json:
  "test_helper": {"spec": ["rspec"]}

And now it runs smoothly. Just run zeus rspec, and you’ll have your specs run as if you called rspec.

As usual, here is the gist with complete Zeus config files (https://gist.github.com/moonfly/5326451):


require 'zeus/rails'
class CustomPlan < Zeus::Rails
def spec(argv=ARGV)
# disable autorun in case the user left it in spec_helper.rb
RSpec::Core::Runner.disable_autorun!
exit RSpec::Core::Runner.run(argv)
end
end
Zeus.plan = CustomPlan.new


{
"command": "ruby -rubygems -r./custom_plan -eZeus.go",
"plan": {
"boot": {
"default_bundle": {
"development_environment": {
"prerake": {"rake": []},
"console": ["c"],
"generate": ["g"]
},
"test_environment": {
"test_helper": {"spec": ["rspec"]}
}
}
}
}
}

view raw

zeus.json

hosted with ❤ by GitHub

Optimizing database access form HABTM join tables

You can save a bunch of SELECT/INSERT commands and minimize data exchange between the DB and your application for HABTM join tables. The optimizations to consider:

  • The HABTM join table is just two IDs cramped together. So work with IDs of the two models that have N-to-N association instead of full records.
  • You can use INSERT INTO ... VALUES (multiple values) to minimize the number of INSERTs to update the table. When a record is just two IDs, constructing the list of multiple such records is easy.

Here is the example. Let’s use the classic setup. In our database we store articles, and each article has tags. There’s clearly N-to-N association between the articles and tags, and we use HABTM in our models.
We need to create a method for Article (called add_tags_from) that takes another article, and adds all tags that exist for that other article but don’t exist for self to self.

We start with a simple code in our Article model:

def diff_tags(other_a)
  other_a.tags - tags
end
def add_tags_from(other_a)
  tags << diff_tags(other_a)
end

It works well:

a1 = Article.find(1)
a2 = Article.find(2)
a1.add_tags_from(a2)

leads to

SELECT "tags".* FROM "tags" INNER JOIN "articles_tags" ON "tags"."id" = "articles_tags"."tag_id" WHERE "articles_tags"."article_id" = ?  [["article_id", 2]]
SELECT "tags".* FROM "tags" INNER JOIN "articles_tags" ON "tags"."id" = "articles_tags"."tag_id" WHERE "articles_tags"."article_id" = ?  [["article_id", 1]]
begin transaction
INSERT INTO "articles_tags" ("article_id", "tag_id") VALUES (1, 5) # assuming tag id 5 was present for a2 but not for a1
INSERT INTO "articles_tags" ("article_id", "tag_id") VALUES (1, 6) # assuming tag id 5 was present for a2 but not for a1
... and all other missing tags ...
commit transaction

Please note that I use Postgres, so your SQL may differ.

Now let’s optimize. First, let’s work with ids instead of entire Articles:

def diff_tags_ids(other_a)
  (other_a.tags.select(:id) - tags.select(:id)).map(&:id)
end

Second, let’s use INSERT with multiple values given tags ids:

def add_tags_ids(tag_ids)
  query_head = 'INSERT INTO "answers_tags" ("answer_id", "tag_id") VALUES '
  query_values = []
  tag_ids.each do |tag_id|
    query_values << "(#{self.id},#{tag_id})"
  end
  query = query_head + query_values.join(", ")
  ActiveRecord::Base.connection.execute(query)
end

And finally, combine the two into our add_tags_from method:

def add_tags_from(other_a)
  add_tags_ids( diff_tags_ids(other_a) )
end

Now the SQL commands for the same example look different:

SELECT id FROM "tags" INNER JOIN "articles_tags" ON "tags"."id" = "articles_tags"."tag_id" WHERE "articles_tags"."article_id" = ?  [["article_id", 2]]
SELECT id FROM "tags" INNER JOIN "articles_tags" ON "tags"."id" = "articles_tags"."tag_id" WHERE "articles_tags"."article_id" = ?  [["article_id", 1]]
INSERT INTO "articles_tags" ("article_id", "tag_id") VALUES (1,5), (1,6) # or whatever values are actually missing in article 1 compared to article 2

First, we are down to 3 SQL statements from 2+N (where N is the number of missing tags). Second we SELECT id instead of SELECT * for tags.

For this simple articles/tags example that’s probably an overkill. The original code was concise and didn’t require constructing and executing the SQL INSERT query manually. But if you have an N-to-N association where N can be dozens, hundreds or thousands, that may be an important performance optimization for you.

Why I like answering questions at StackOverflow

Why I like looking through StackOverflow and answering interesting questions there is because I learn something new each time I do that – something that I haven’t needed yet on my current projects, but that may be useful in the future. In most cases I don’t know the answer right away, so I experiment for some minutes in Rails console inside my ‘experiments’ project that I always have on my machine to try various approaches before using them in my applications. I wonder why the people who ask the questions don’t experiment themselves.

Here are just a few examples of the things that I didn’t know but quickly learned while answering other people’s questions – how to …

Array#replace for HABTM associations

I was recently answering a question at StackOverflow.

(I know nobody cares, but just can’t stop myself from commenting as a by-the-way: I was clearly hoping to get the bounty ;), but I misunderstood what was meant by optimized solution, and tried to answer the original question not realizing that what was actually needed is not only adding new tags, but also removing unneeded tags).

Anyways, it’s not about me not answering the right question. The (well-deserved) best answer contained a reference to the not-so-well-known (to me) Array#replace method useful when we need to update the association records (and all credits for this approach should go to the author of that answer). What it does is replacing the contents of the original array with the new contents passed as the method parameter, like this:

x = ["a","b","c"]
x.replace(["b","c","d","e"])
x #=> ["b","c","d","e"]

Where it works well is in the situations like this:

# Replace the tags of article#2 with the tags of article#1
# assuming there is HABTM association between Tag and Article models
a1 = Article.find(1)
a2 = Article.find(2)
a2.tags.replace(a1.tags)

Enabling PostgreSQL Hstore with Rails 4

Rails 4 now supports several native PostgreSQL types out of the box. That includes Hstore, Array, network address types, and several others. See a good overview in http://blog.remarkablelabs.com/2012/12/a-love-affair-with-postgresql-rails-4-countdown-to-2013, and hstore-focused post at http://schneems.com/post/19298469372/you-got-nosql-in-my-postgres-using-hstore-in-rails. However, there is one small gotcha.

If you do the following in your migrations without enabling the “hstore” extension first:

class CreateThings < ActiveRecord::Migration
  def change
    create_table :things do |t|
      t.hstore :data
    end
  end
end

you’ll get an error:

PG::Error: ERROR:  type "hstore" does not exist

That’s not news. Ryan Bates in his Railscasts and the posts listed above suggest creating a special migration to enable Hstore. Something like (I used the version that I liked most – from Railscasts):

class SetupHstore < ActiveRecord::Migration
  def self.up
    execute "CREATE EXTENSION IF NOT EXISTS hstore"
  end
  
  def self.down
    execute "DROP EXTENSION IF EXISTS hstore"
  end
end

After you do it (obviously, that migration should precede any migrations that use the hstore type), you’re good. Moreover, you don’t even need to change your schema format to SQL (using `config.active_record.schema_format = :sql` in your application.rb). Rails automatically detects what you want to do with that migration and creates the following line in your schema.rb file:

  enable_extension "hstore"

Brilliant Rails magic, as usually! But that fact also opened my eyes that there is a more concise way to enable Hstore for your Rails application. Instead of that SetupHstore migration with native execute “…” commands, you can write a simpler migration that has the same effect (gist at https://gist.github.com/moonfly/5183843):


class SetupHstore < ActiveRecord::Migration
def self.up
enable_extension "hstore"
end
def self.down
disable_extension "hstore"
end
end

view raw

setup_hstore.rb

hosted with ❤ by GitHub

It works.

UPDATE: Originally I was just doing enable_extension inside the create method, but I quickly noticed that it doesn’t rollback correctly, i.e. the enable_extension “hstore” line is not removed from schema.rb. Though for most situations that’s not a big deal – you just have this extension enabled even if you don’t need it anymore after rollbacks – I still decided to use a correct version, consisting of self.up with enable_extension and self.down with disable_extension.

Testing cookies and remember_me functionality with RSpec/Capybara

I know I’m overdoing it, but since in my projects I often implement custom authentication or use Devise in non-trivial ways, I like my feature tests to test all aspects of authentication including remember_me functionality (you know, that checkbox that saves an auth_token in a persistent cookie and logs you in automatically the next time you open your app in a browser).

The thing is: you somehow need to imitate closing a browser and then opening it again. To me, for the scenario in hand, that means precisely: session cookies are deleted, persistent cookies are preserved.

I use RSpec+Capybara+(Rack::Test/Poltergeist) combination for my testing. Capybara doesn’t provide a standard interface for working with cookies (there were some pull requests, but it’s still the case). Some drivers do provide some pieces of the interface, but in different ways, and usually the interface is not enough to do what I need to do, i.e. filter out all session cookies and preserve the rest.

So, I went ahead and implemented my own mini-library that I place in spec/support/cookies. I use some private internals of the Capybara drivers (though, I tried to keep that to minimum), so this is not a clean solution, but rather a hack. Still, I find it useful. Now in my specs I can do:

scenario "remember_me" do
  # ... login for the first time with remember_me NOT set ...
  
  imitate_new_browser_session # this is a call provided by my mini library
  
  # ... go to the protected page and check that I'm being asked to authorize ...
  # ... login again with remember_me set this time ...
  
  imitate_new_browser_session

  # ... go to the protected page and check that I'm being let through to it
  # as an authorized user ...
end

Pretty cool!

Now, here is my code published as a gist (https://gist.github.com/moonfly/5058959). You will find the entire source code, sample feature spec, and some usage notes in usage.txt there:


require 'spec_helper'
feature "Login" do
scenario "remembered user" do
password = "12345678"
user = create(:user, password: password)
access_protected_page_should_ask_to_login
current_path.should eq login_path
login_should_succeed user.email, password, true
imitate_new_browser_session # Note the use of function from my cookie-testing mini-library
access_protected_page_should_not_ask_to_login
current_path.should eq protected_page_path
end
end


class SessionCookies
def self.for(session)
klass = drivers[Capybara.current_driver]
if klass
return klass.new(session)
else
# :selenium, :webkit
raise "Session cookies handling is not supported for driver #{Capybara.current_driver.to_s}"
end
end
def self.register(driver,klass)
@drivers ||= {}
@drivers[driver] = klass
end
def self.drivers
@drivers ||= {}
end
def self.next_session_number
@session_number ||= 0
@session_number += 1
end
end
def copy_cookies(options={})
session_to = options[:to] || Capybara.current_session
session_from = options[:from] || Capybara.current_session
partial = options[:only]
if session_to != session_from
SessionCookies.for(session_to).copy_from SessionCookies.for(session_from), partial
end
end
def clear_cookies(options={})
session = options[:in] || Capybara.current_session
partial = options[:only]
SessionCookies.for(session).clear partial
end
def cookie_value(name,options={})
session = options[:in] || Capybara.current_session
SessionCookies.for(session).cookie_value(name)
end
def clear_cookie(name,options={})
session = options[:in] || Capybara.current_session
SessionCookies.for(session).clear_cookie(name)
end
def all_cookies(options={})
session = options[:in] || Capybara.current_session
SessionCookies.for(session).all_cookies
end
def imitate_new_browser_session(options={})
options[:only] = :session
clear_cookies options
end
def start_new_session(options={})
options[:from] ||= Capybara.current_session
options[:name] ||= "secondary-#{SessionCookies.next_session_number}".to_sym
new_session = nil
Capybara.using_session options[:name] do
new_session = Capybara.current_session
copy_cookies to: new_session, from: options[:from], only: :persistent
yield if block_given?
end
new_session
end
# Common code
class SessionCookies
def initialize(session=Capybara.current_session)
@session = session
end
def copy_from(sc,partial=nil)
if (partial == nil) && respond_to?(:copy_all_from,true)
self.copy_all_from sc
else
ck = sc.cookie_objects
if partial==:persistent
ck.reject! { |name,cookie| !cookie_is_persistent(cookie) }
elsif partial==:session
ck.reject! { |name,cookie| !cookie_is_session(cookie) }
end
ck.each do |name,cookie|
add_cookie_object cookie
end
end
end
def clear(partial=nil)
if (partial == nil) && respond_to?(:clear_all,true)
self.clear_all
else
ck = cookie_objects
if partial==:persistent
ck.reject! { |name,cookie| !cookie_is_persistent(cookie) }
elsif partial==:session
ck.reject! { |name,cookie| !cookie_is_session(cookie) }
end
ck.each do |name,cookie|
clear_cookie name
end
end
end
def cookie_value(name)
cookie = cookie_objects[name]
cookie && cookie.value
end
def all_cookies
ck = {}
cookie_objects.each do |name,cookie|
ck.merge!( name => cookie_value(name) )
end
ck
end
def cookie_objects
raise "SessionCookies: cookie_objects should be implemented in a driver-specific class"
end
protected
def cookie_is_session(cookie)
cookie.expires == nil
rescue
true
end
def cookie_is_persistent(cookie)
!cookie_is_session(cookie)
end
end


class PoltergeistSessionCookies < SessionCookies
def initialize(session=Capybara.current_session)
super(session)
end
def clear_cookie(name)
@session.driver.remove_cookie(name)
end
protected
def cookie_objects
@session.driver.cookies
end
def add_cookie_object(cookie)
@session.driver.set_cookie( cookie.name, cookie.value, cookie_options_for(cookie) )
end
def cookie_options_for(cookie)
options = {}
defs = {
:name=>:name,
:value=>:value,
:domain=>:domain,
:path=>:path,
:secure=>:secure?,
:httponly=>:httponly?,
:expires=>:expires
}
defs.each do |name,method|
options[name] = cookie.send(method)
end
options
end
end
SessionCookies.register :poltergeist, PoltergeistSessionCookies


module Rack
module Test
class CookieJar
def cookie_objects
hash_for nil
end
end
end
end
class RackTestSessionCookies < SessionCookies
def initialize(session=Capybara.current_session)
@mock_session = session.driver.browser.current_session.instance_variable_get(:@rack_mock_session)
super(session)
end
def copy_all_from(sc)
self.jar = sc.jar
end
def clear_all
@mock_session.clear_cookies
end
def clear_cookie(name)
jar.delete(name)
end
def cookie_value(name)
jar.to_hash[name]
end
protected
def add_cookie_object(cookie)
jar << cookie
end
def jar
@mock_session.cookie_jar
end
def jar=(value)
@mock_session.cookie_jar = value
end
def cookie_objects
jar.cookie_objects
end
end
SessionCookies.register :rack_test, RackTestSessionCookies


In your spec files you can call:
* copy_cookies [:to => session2,] [:from => session1,] [:only => cookies_type] # copy cookies of type :persistent|:session|:all from session1 to session2
* clear_cookies [:in => session,] [:only => cookies_type] # clear all cookies of a given type in a given session
* cookie_value "name", [:in => session] # get cookie value
* clear_cookie "name", [:in => session] # clear specific cookie
* all_cookies [:in => session] # get all cookies
* imitate_new_browser_session [:only => cookies_type] # clear cookies of specified type in this session, by default only session cookies are cleared, and persistent are preserved, as in real life
* start_new_session [:name=>"session_name",] [:from=>session] [ { yield } ] # create new Capybara session (with a given name), copy persistent cookies there from the specified session (current by default), and execute a block in it if block given
In all cases if session is omitted, the current session is taken, if cookies_type is omitted, :all is assumed (unless stated otherwise).

view raw

usage.txt

hosted with ❤ by GitHub

Devise with Rails 3.2.x and strong_parameters

I like being prepared. So, while working on 3.2.x apps, in my Gemfile I have the following lines (plus a few other config lines here and there):

# RAILS4: Prepare for Rails 4.0
gem 'strong_parameters'
gem 'cache_digests'
gem 'turbolinks'
# POSTGRESQL: use HStore in PG, already a part of Rails 4.0
gem 'activerecord-postgres-hstore'

And in my initializers:

module MyApplicationName
  class Application < Rails::Application
    # RAILS4: use 'strong_parameters' instead
    config.active_record.whitelist_attributes = false
  end
end

# RAILS4: use 'strong_parameters'
ActiveRecord::Base.send(:include, ActiveModel::ForbiddenAttributesProtection)

And I use Devise (https://github.com/plataformatec/devise) for authentication whenever possible. The thing is, while there is a work going on Rails 4 support for Devise, the Rails 3.2.x Devise doesn’t know how to deal with params if you include ActiveModel::ForbiddenAttributesProtection. Fortunately, there is a resource_params method that Devise controllers use, so I overwrite it to use ‘permit’ approach of strong_parameters. Here is the gist (https://gist.github.com/moonfly/5049487):


class RegistrationsController < Devise::RegistrationsController
def resource_params
permit_keys = [ :password, :password_confirmation ] + resource_class.authentication_keys
params.require(resource_name).permit( *permit_keys )
end
end


devise_for :users, :controllers => { :registrations => "registrations" }

view raw

routes.rb

hosted with ❤ by GitHub

A path to fast and honest tests – RSpec and Database Cleaner

You normally want your tests to be fast, and that means fast setup before each test & fast cleanup after each test. No brainer: use transactions in testing, that’s faster than cleaning up the database through records deletion or base truncation.

But sometimes you want to try things out as honestly as possible – going through all layers of your app w/o any mockups and injections. And that means testing through accessing real pages with Javascript on them. And that means using engines like Selenium or PhantomJS on top of Capybara. One consequence of that is you can’t use transactions – they are not seen in a separate database connection established by those engines. Check the (as always excellent) Railscast on the subject: http://railscasts.com/episodes/257-request-specs-and-capybara.

Well, you can use tricks to work around that – check the well-known 3rd tip in http://blog.plataformatec.com.br/2011/12/three-tips-to-improve-the-performance-of-your-test-suite/. But, as I said, I like things to be as honest as possible, and those tricks with possible side-effects (I guess, everybody who was interested in the subj also checked http://www.spacevatican.org/2012/8/18/threading-the-rat/) don’t seem right to me. If I decided to include that slow spec that uses JS in my test run, that means it’s worth the time to do it honestly, so I don’t want to micro-optimize and undermine the main purpose of these tests – do honest end-to-end verification as on the real system. That means (for me) using database truncation/records deletion instead of transactions.

There is also another case – you may want to test the code that works only after the commit is made (i.e. in ‘after_commit’ callbacks). One example: I wanted to system-test the entire auth framework of my app which was based on devise with asynchronous email sending based on devise-async. And devise-async uses ‘after_commit’. Again, transactions-based cleanup won’t work here.

At the same time, those tests that need a javascript engine or verify after_commit are only a small portion of my test suite. I still want the rest of the tests to be as fast as possible and use transactions. My solution: DatabaseCleaner (https://github.com/bmabey/database_cleaner) + choosing the cleaning strategy based on the spec tags + optimizing for the common case (when the tests use transactions). I’m not the 1st person to utilize this strategy. I’m not even in the first 1000. But you never stop improving. Most approaches just set the strategy before each test run based on the tags. That works, but think what you do: each time by calling ‘strategy=’ you create a strategy object underneath – it eats time and gives work to GC (another way to eat time). And remember we want the test execution to be optimized for the common case (when there are a lot of simple tests that use transactions for cleanup are run side-by-side, with only a few ‘:js => true’ tests among them). So, in my approach I keep the strategy set to transaction all the time, but simply don’t use it (don’t call start/clean) for JS/commit tests, and call ‘clean_with’ after the test run.

Here is the gist (https://gist.github.com/moonfly/4950750) of my RSpec + Database Cleaner approach. There was also some reading involved in deciding if I want to use truncation or deletion as my fallback strategy for JS/commit. I use Postgres, so I followed the advice (http://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886) and used deletion as the main method, doing truncation only once per the entire test suite run.


config.use_transactional_fixtures = false
config.before(:suite) do
# Do truncation once per suite to vacuum for Postgres
DatabaseCleaner.clean_with :truncation
# Normally do transactions-based cleanup
DatabaseCleaner.strategy = :transaction
end
config.around(:each) do |spec|
if spec.metadata[:js] || spec.metadata[:test_commit]
# JS => run with PhantomJS that doesn't share connections => can't use transactions
# deletion is often faster than truncation on Postgres – doesn't vacuum
# no need to 'start', clean_with is sufficient for deletion
# Devise Emails: devise-async sends confirmation on commit only! => can't use transactions
spec.run
DatabaseCleaner.clean_with :deletion
else
# No JS/Devise => run with Rack::Test => transactions are ok
DatabaseCleaner.start
spec.run
DatabaseCleaner.clean
# see https://github.com/bmabey/database_cleaner/issues/99
begin
ActiveRecord::Base.connection.send(:rollback_transaction_records, true)
rescue
end
end
end

view raw

spec_helper.rb

hosted with ❤ by GitHub