giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Tagged:

Comments (View)

Testing a before_filter

Recently, we implemented a feature that required a before_filter in ApplicationController and whitelisting some other controllers using skip_before_filter.

We couldn’t actually test something like this directly because callbacks aren’t really methods. They’re entirely used for their side effects so we can only test what happens when we invoke one. Let’s check out an example:

require 'test_helper'

class ApplicationControllerTest < ActionController::TestCase
  context 'ensure_manually_set_password' do
    setup do
      class ::TestingController < ApplicationController
        def hello
          render :nothing => true
        end
      end

      ActionController::Routing::Routes.draw do |map|
        map.hello '', :controller => 'testing', :action => 'hello'
      end
    end

    teardown do
      Object.send(:remove_const, :TestingController)
    end

    context 'when user is logged in' do
      setup do
        @controller = TestingController.new
      end

      context 'and user has not manually set their password' do
        setup do
          @user = Factory(:user, :manually_set_password => false)
          login_as @user
          get :hello
        end

        should 'redirect user to set their password' do
          assert_redirected_to new_password_path(@user.password_token)
        end
      end
    end
  end
end

Note the use of the double-colon prepended to the TestingController, which ensures the class is top-level, not an inner class of ApplicationControllerTest. That way we can just do :controller => 'testing' and not have to write :controller => 'application_controller_test/testing'. We also use the private method remove_const to remove the class after we’re done, so we don’t litter the namespace.

Co-written with Gabe Berke-Williams

Tagged:

Comments (View)

Short, explicit test setups

You probably know about this Factory Girl definition syntax:

FactoryGirl.define do
  factory :user do
    name 'Connie Customer'
  end
end

But did you know about this Factory Girl invocation syntax?

setup do
  @user = create(:user)
end

Or:

setup do
  @user = build(:user)
end

Or:

setup do
  post :create, user: attributes_for(:user)
end

It’s in there.

Configuration for Test::Unit / Shoulda:

class ActiveSupport::TestCase
  include FactoryGirl::Syntax::Methods
end

Configuration for RSpec:

RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end

Configuration for Cucumber:

World FactoryGirl::Syntax::Methods

Tagged:

Comments (View)

How to apply what you’ve learned from TDD to writing data migrations

After doing TDD full time for years, I have a hard time writing code without a test. One example that I find particularly difficult is writing data migrations.

Some schema changes require more than just setting a default value for all existing rows. For example, let’s say you have this schema:

create_table :users do |table|
  table.string :email
  table.string :encrypted_password
end

create_table :posts do |table|
  table.integer :user_id
  table.boolean :published
  table.string :message
end

If you want to find the top ten users based on the number of published posts, you can do a JOIN with a COUNT and a GROUP BY clause, but over time that could get slow or just annoying, so you decide to add a cache column:

add_column :users, :published_posts_count, :integer, :default => 0, :null => false

You add code to populate the column and all the tests pass, but of course there’s a big problem: every existing user in production will report zero published posts. That means it’s time to break out a data migration. Running migrations over and over with different data or comparing lots of queries before and after migrating production data is tedious and error-prone, so let’s write a throw-away test for this migration. You can save this as spec/migration_spec.rb:

require 'spec_helper'
require Dir.glob(Rails.root.join("db", "migrate", "*_set_published_posts_for_existing_users.rb")).first

describe SetPublishedPostsForExistingUsers do
  it "counts only published posts" do
    user = FactoryGirl.create(:user)
    FactoryGirl.create_list(:post, 3, :user => user, :published => true)
    reset_cache_and_run_migration
    user.reload.published_posts_count.should == 3
  end

  def reset_cache_and_run_migration
    User.update_all("published_posts = 0")
    SetPublishedPostsForExistingUsers.new.up
  end
end

Add an empty migration, and the test fails because the user is reporting no published posts. We can get this test passing with a simple migration:

class SetActivatedFlagForExistingUsers < ActiveRecord::Migration
  def up
    connection.update(<<-SQL)
      UPDATE users
      SET published_posts_count = (
        SELECT COUNT(*) FROM posts
      )
    SQL
  end

  def down
    # No problem
  end
end

Next up, we need to make sure it’s only counting published posts:

it "counts only published posts" do
  user = FactoryGirl.create(:user)
  FactoryGirl.create_list(:post, 3, :user => user, :published => true)
  FactoryGirl.create(:post, :user => user, :published => false)
  reset_cache_and_run_migration
  user.reload.published_posts_count.should == 3
end

That will fail because the migration counts the published posts, ending up with a total of four. We can fix that easily:

def up
  connection.update(<<-SQL)
    UPDATE users
    SET published_posts_count = (
      SELECT COUNT(*) FROM posts
      WHERE posts.published = true
    )
  SQL
end

Next up, we need to make sure each user only counts their own posts, so we add a post for a different user:

it "counts only published posts" do
  user = FactoryGirl.create(:user)
  FactoryGirl.create_list(:post, 3, :user => user, :published => true)
  FactoryGirl.create(:post, :user => user, :published => false)
  other_user = FactoryGirl.create(:user)
  FactoryGirl.create(:post, :user => other_user, :published => true)
  reset_cache_and_run_migration
  user.reload.published_posts_count.should == 3
end

The test fails again with a count of four, since it picked up the other user’s post. Getting this test to pass leads to our final migration:

def up
  connection.update(<<-SQL)
    UPDATE users
    SET published_posts_count = (
      SELECT COUNT(*) FROM posts
      WHERE posts.published = true
      AND posts.user_id = users.id
    )
  SQL
end

At this point, I just delete the spec. Since migrations should never be edited after they run, there’s little reason to test for regressions. Inevitably the schema will change, which will mean the spec no longer applies.

The spec provides no value after the migration is committed, but I still find writing specs like these well worth the time. It’s easier for me to think like I’m used to, by writing tests first, and it makes me confident that the migration actually covers the cases it’s supposed to.

Tagged:

Comments (View)

Apprentice.io Three Week Retrospective

Over the past three weeks I’ve begun my apprenticeship at thoughtbot. The apprenticeship lasts until the end of March.  I’m joined by designers Paul Webb and Edwin Morris, and fellow coder apprentice Alex Patriquin. Each of us is assigned to a mentor, who makes sure we absorb as much as possible of the thoughtbot way of doing things and achieve our specific goals. For me, the apprenticeship is a couple of things: it’s a chance to work in coder nirvana (TDD, heavy refactoring, bookshelf full of great literature, 5-minute meetings, pair programming, investment days, open source - the works), and it’s a chance to vastly increase my Rails skills in a bootcamp-style training environment.

My head is swimming with new knowledge. I entered this state the first day, and I’ve been there constantly for the past three weeks. Each evening I ride the T home with my poor neurons about to burst with new activity. At night while I sleep my brain indexes all this new knowledge, and the next day I dive in again. If you’ve ever read Ender’s Game, it’s sort of like Battle School for Geeks.

Let’s get specific. This list is long because I’ve been truly busy. In the past three weeks I have:

  • Mercilessly refactored my code two, three, even four times after writing it. The standards for code quality here are very, very high.
  • Learned to use Github pull requests for code review.
  • Responded to around 150 code review comments, most of which made me stop and rethink coding habits I’ve held unconsciously for years. My style is improving fast.
  • Participated daily in a morning company-wide standup that actually takes less than five minutes, and a team standup that takes another five. That’s only ten minutes of meetings per day!
  • Switched to using higher-level cucumber tests involving steps like “When I sign up as a new user” as opposed to “When I click the link marked ‘foo’”.
  • Achieved and maintained 100% perfect TDD discipline - not writing a single line of feature code until a failing test is present.
  • Completely switched text editors, from Textmate to Vim.
  • Became faster in vim than I ever was in Textmate.
  • Written custom functions for vim.
  • Learned other new tools including Homebrew, ack, ctags, New Relic, KISSmetrics, and kumade.
  • Forked thoughtbot’s dotfiles repo and started customizing it as my personal dev environment. The dotfiles repo contains bashrc, vimrc, git-config, alises, etc - I can now install it to any computer I use to instantly have my customized dev environment in place.
  • Watched an app’s user base jump from 100 to 10,000 in a matter of minutes as an article hit TechCrunch.
  • Gotten a crash-course in startup funding from the other coding apprentice, Alex Patriquin, who boasts, quite accurately, of knowing everybody.
  • Participated in discussions about new features in rails.
  • Made my very first contribution to an open source repo.
  • Been approved to give my first professional presentation - a lightning talk at at Boston.rb meeting.
  • Consumed 3,000 cups of green tea and a foie gras taco.

Did I mention I’ve been busy? That’s just the first three weeks - and I’ve got ten weeks left. I feel excited, involved, and challenged in a completely invigorating way. The team here is universally smart and helpful, and while I’m here I simply can’t help but get better by osmosis at what I do. This is the most fun I’ve had in an office in a long time.

For more information about this program, and to sign up to sponsor a pool of apprentices, visit apprentice.io

More later on,