
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.
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
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.
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:
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,